﻿/*
 * HyperStat.NET - The minimalist's static website compiler.
 * Copyright (C) 2012 Benjamin Brent
 * http://www.hyperstat.net/
 * http://hyperstat.codeplex.com/
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using dotless.Core;
using System.Text.RegularExpressions;

namespace HyperStat
{
    class HyperStatSettings
    {
        #region Private Fields

        private Dictionary<string, string> content = new Dictionary<string, string>();
        private Dictionary<string, string> contentPaths = new Dictionary<string, string>();
        private List<string> fileCopy = new List<string>();
        private string projectPath;
        private string cssNavActive = string.Empty;
        private int cssNavLayer = 0;
        private bool prettify = false;
        private Dictionary<string, string> userVars = new Dictionary<string, string>();
        private bool htmlCompress = false;
        private bool cssCompress = false;
        private bool jsCompress = false;
        private Dictionary<string, HookLocation> hooks = new Dictionary<string, HookLocation>();

        // Content extensions to search for, in order of preference
        private Dictionary<string, ContentType> contentExts = new Dictionary<string, ContentType>()
        {
            {"html"     , ContentType.HTML    },
            {"mkd"      , ContentType.Markdown},
            {"md"       , ContentType.Markdown},
            {"markdown" , ContentType.Markdown},
        };

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new settings object from the given settings file.
        /// </summary>
        /// <param name="projectPath">Path to the project dir.</param>
        public HyperStatSettings(string projectPath, string settingsPath)
        {
            this.projectPath = projectPath;

            string configText = File.ReadAllText(settingsPath, Encoding.UTF8);
            string[] lines = Regex.Split(configText, "\r\n|\r|\n");
            string key = string.Empty;
            foreach (string lineraw in lines)
            {
                string line = lineraw.Replace("\0", string.Empty);

                // Ignore empty lines
                if (line.Length < 1 || line == string.Empty)
                    continue;

                // Ignore comment lines
                if ((line.Length > 1 && line[0] == ';' || line[0] == '#' || (line[0] == '/' && line[1] == '/')) || line.Length < 1)
                    continue;

                // Parse keyword line
                if (line.Length > 2 && line[0] == '[' && line[line.Length - 1] == ']')
                {
                    key = line.ToUpper();
                    continue;
                }

                // Parse data lines into appropriate categories
                char[] trim = { '/', '\\' };
                switch (key)
                {
                    // List of content files
                    case "[CONTENT]":
                        string[] contentProperties = Regex.Split(line, "::");
                        string[] pathComponents = Regex.Split(contentProperties[0].TrimStart(trim), @"[\/\\]");
                        contentPaths.Add(pathComponents[pathComponents.Length - 1], contentProperties[0].TrimStart(trim));
                        content.Add(pathComponents[pathComponents.Length - 1], contentProperties[1]);
                        break;

                    // Navigation settings
                    case "[NAVIGATION]":
                        line = line.Replace(" ", string.Empty);
                        string[] parts = Regex.Split(line, "=");

                        // Parse the active CSS class
                        if (parts[0].ToUpper() == "CSS_ACTIVE_CLASS")
                            cssNavActive = parts[1];

                        // Parse the active layer
                        // NOTE: Can be +/- integer, outer (string), or inner (string)
                        else if (parts[0].ToUpper() == "CSS_ACTIVE_LAYER")
                        {
                            bool result = int.TryParse(parts[1], out cssNavLayer);
                            if (!result)
                            {
                                if (parts[1].Equals("outer", StringComparison.OrdinalIgnoreCase))
                                    cssNavLayer = 1;
                                else if (parts[1].Equals("inner", StringComparison.OrdinalIgnoreCase))
                                    cssNavLayer = -1;
                                else if (parts[1].Equals("default", StringComparison.OrdinalIgnoreCase))
                                    cssNavLayer = 0;
                            }
                        }
                        break;

                    // Misc settings
                    case "[MISC]":
                        string[] miscParts = Regex.Split(line, "=");
                        for (int i = 0; i < miscParts.Length; i++)
                            miscParts[i] = miscParts[i].Trim();
                        if (miscParts.Length != 2)
                            break;
                        // Prettify functionality
                        else if (miscParts[0].Equals("PRETTIFY", StringComparison.OrdinalIgnoreCase) &&
                            Boolean.TryParse(miscParts[1], out prettify))
                            break;
                        // Set output compression
                        else if (miscParts[0].Equals("COMPRESSION", StringComparison.OrdinalIgnoreCase))
                            SetCompression(miscParts[1]);
                        break;

                    // Allow user-defined variables
                    case "[VARIABLES]":
                        string[] varParts = line.Split(new char[]{'='}, 2);
                        for (int i = 0; i < varParts.Length; i++)
                            varParts[i] = varParts[i].Trim();
                        if (varParts.Length != 2)
                            break;
                        userVars.Add(varParts[0], varParts[1]);
                        break;

                    // Files or dirs to recursively copy to output
                    case "[COPY]":
                        string path = Path.Combine(projectPath, line);
                        FileInfo finfo = new FileInfo(path);
                        if(File.Exists(path) || Directory.Exists(path))
                            fileCopy.Add(finfo.FullName.Replace(projectPath, string.Empty).TrimStart(trim));
                        break;
                    
                    // Hook scripts support
                    case "[HOOKS]":
                        string[] hooks = Regex.Split(line, "=");
                        for (int i = 0; i < hooks.Length; i++)
                            hooks[i] = hooks[i].Trim();
                        if (hooks.Length != 2)
                            break;
                        else if (hooks[0].Equals("PRE_COMPILE", StringComparison.OrdinalIgnoreCase))
                            this.hooks.Add(hooks[1], HookLocation.PreCompile);
                        else if (hooks[0].Equals("POST_COMPILE", StringComparison.OrdinalIgnoreCase))
                            this.hooks.Add(hooks[1], HookLocation.PostCompile);
                        break;
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Sets the compression settings.
        /// <param name="rawConf">A comma-separated list of values.</param>
        /// </summary>
        private void SetCompression(string rawConf)
        {
            string[] vals = rawConf.Split(',');
            for (int i = 0; i < vals.Length; i++)
                vals[i] = vals[i].Trim();
            if (vals.Length == 0)
                return;
            foreach (string val in vals)
            {
                if (val.Equals("html", StringComparison.OrdinalIgnoreCase))
                    htmlCompress = true;
                else if (val.Equals("css", StringComparison.OrdinalIgnoreCase))
                    cssCompress = true;
                else if (val.Equals("js", StringComparison.OrdinalIgnoreCase))
                    jsCompress = true;
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Content extensions, in order of preference.
        /// </summary>
        public Dictionary<string, ContentType> ContentExts
        {
            get { return contentExts; }
        }

        /// <summary>
        /// Indicates whether HyperStat is in verbose mode or not (true for yes).
        /// </summary>
        public bool Verbose
        {
            get { return (Program.verbosity > 0); }
        }

        /// <summary>
        /// Contains a list of hooks.
        /// </summary>
        public Dictionary<string, HookLocation> Hooks
        {
            get { return hooks; }
        }

        /// <summary>
        /// Indicates whether HTML should be compressed or not.
        /// </summary>
        public bool HtmlCompress
        {
            get { return htmlCompress; }
        }

        /// <summary>
        /// Indicates whether CSS should be compressed or not.
        /// </summary>
        public bool CssCompress
        {
            get { return cssCompress; }
        }

        /// <summary>
        /// Indicates whether JS should be compressed or not.
        /// </summary>
        public bool JsCompress
        {
            get { return jsCompress; }
        }

        /// <summary>
        /// User-defined variables, inserted with {{v:VARNAME}}.
        /// </summary>
        public Dictionary<string, string> UserVars
        {
            get { return userVars; }
        }

        /// <summary>
        /// The layer to apply the navigation 'active' class to. 0 represents the link layer,
        /// positive integers represent outer layers, and negative integers represent inner layers.
        /// </summary>
        public int CssNavLayer
        {
            get { return cssNavLayer; }
        }

        /// <summary>
        /// A list of relative filepaths of files which should be copied to the output directory.
        /// </summary>
        public List<string> FileCopy
        {
            get { return fileCopy; }
        }

        /// <summary>
        /// The CSS class to be applied to active navigation links.
        /// </summary>
        public string CssNavActive
        {
            get { return cssNavActive; }
        }

        /// <summary>
        /// List of content file paths.
        /// key   = name/ID
        /// value = title
        /// </summary>
        public Dictionary<string, string> Content
        {
            get { return content; }
        }

        /// <summary>
        /// If enabled, pre tags will be given prettify classes.
        /// </summary>
        public bool Prettify
        {
            get { return prettify; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Generates the output path for a given content file.
        /// </summary>
        public string MakeCtntOutPath(string outputPath, string ctntKey, bool relative = true)
        {
            if (relative)
                return (contentPaths[ctntKey] + ".html");
            return (Path.Combine(outputPath, contentPaths[ctntKey]) + ".html");
        }

        /// <summary>
        /// Generates the full output path to a given CSS file.
        /// </summary>
        public string MakeCssOutPath(string outputPath, string fileName, bool keepOrigExtension = false)
        {
            if (keepOrigExtension)
                return Path.Combine(outputPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.CSS_DIR + Path.DirectorySeparatorChar, fileName);
            return Util.ChangeExtension(Path.Combine(outputPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.CSS_DIR + Path.DirectorySeparatorChar, fileName), "css");
        }

        /// <summary>
        /// Generates the full output path to a given JS file.
        /// </summary>
        public string MakeJsOutPath(string outputPath, string fileName, bool keepOrigExtension = false)
        {
            if (keepOrigExtension)
                return Path.Combine(outputPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.JS_DIR + Path.DirectorySeparatorChar, fileName);
            return Util.ChangeExtension(Path.Combine(outputPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.JS_DIR + Path.DirectorySeparatorChar, fileName), "js");
        }

        /// <summary>
        /// Generates the full output path to a given IMG file.
        /// </summary>
        public string MakeImgOutPath(string outputPath, string fileName)
        {
            return Path.Combine(outputPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.IMG_DIR + Path.DirectorySeparatorChar, fileName);
        }

        /// <summary>
        /// Generates the full output path to a given style (CSS resource) file.
        /// </summary>
        public string MakeStyleOutPath(string outputPath, string fileName)
        {
            return Path.Combine(outputPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.STYLE_DIR + Path.DirectorySeparatorChar, fileName);
        }

        /// <summary>
        /// Generates the full input path to a given style (CSS resource) file.
        /// </summary>
        public string MakeStyleInPath(string fileName)
        {
            return Path.Combine(projectPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.STYLE_DIR, fileName);
        }

        /// <summary>
        /// Generates the full input path to a given IMG file.
        /// </summary>
        public string MakeImgInPath(string fileName)
        {
            return Path.Combine(projectPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.IMG_DIR, fileName);
        }

        /// <summary>
        /// Generates the full input path to a given CSS file.
        /// </summary>
        public string MakeCssInPath(string fileName)
        {
            return Path.Combine(projectPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.CSS_DIR + Path.DirectorySeparatorChar, fileName);
        }

        /// <summary>
        /// Generates the full input path to a given JS file.
        /// </summary>
        public string MakeJsInPath(string fileName)
        {
            return Path.Combine(projectPath + Path.DirectorySeparatorChar + StaticHtmlGenerator.JS_DIR + Path.DirectorySeparatorChar, fileName);
        }

        /// <summary>
        /// Generates the full path to a given content file.
        /// </summary>
        /// <param name="ctntKey">The key of the content file from the content dictionary.</param>
        public string MakeCtntInPath(string ctntKey)
        {
            ContentType type;
            return MakeCtntInPath(ctntKey, out type);
        }

        /// <summary>
        /// Generates the full path to a given content file.
        /// </summary>
        /// <param name="ctntKey">The key of the content file from the content dictionary.</param>
        public string MakeCtntInPath(string ctntKey, out ContentType ctntType)
        {
            string basePath = Path.Combine(projectPath, StaticHtmlGenerator.CONTENT_DIR + Path.DirectorySeparatorChar + ctntKey);
            DirectoryInfo contentDir = new DirectoryInfo(Path.Combine(projectPath, StaticHtmlGenerator.CONTENT_DIR));

            foreach (string ext in contentExts.Keys)
            {
                Regex fileSearchPattern = new Regex(String.Format(@"^.*{0}\.{1}.*$", ctntKey, ext), RegexOptions.IgnoreCase);
                FileInfo[] possibleFiles = contentDir.GetFiles(fileSearchPattern);

                foreach(FileInfo possibleFile in possibleFiles)
                {
                    if (Path.GetFileNameWithoutExtension(possibleFile.Name).Equals(ctntKey, StringComparison.OrdinalIgnoreCase))
                    {
                        ctntType = contentExts[ext];
                        return possibleFile.FullName;
                    }
                }
            }

            throw new HyperStatException("Content file not found: " + ctntKey);
        }

        #endregion

        #region Enums

        /// <summary>
        /// Represents the type of content in a content file.
        /// </summary>
        public enum ContentType
        {
            /// <summary>
            /// HTML Content only.
            /// </summary>
            HTML,

            /// <summary>
            /// Markdown/HTML content.
            /// </summary>
            Markdown
        }

        /// <summary>
        /// Hook locations
        /// </summary>
        public enum HookLocation
        {
            PreCompile,
            PostCompile
        }

        #endregion
    }
}
